package yipdf;

import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;

/**
 * Quick and dirty Java Swing front-end for some pdfjam features...
 * Needs pdfjam in user PATH
 */
public class YiPDF extends javax.swing.JFrame {

    private static final String VERSION="1.0";

    public YiPDF() {
        initComponents();
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {
        java.awt.GridBagConstraints gridBagConstraints;

        jPanel1 = new javax.swing.JPanel();
        fileLabel = new javax.swing.JLabel();
        fileField = new javax.swing.JTextField();
        browseButton = new javax.swing.JButton();
        trimCheckBox = new javax.swing.JCheckBox();
        trimTextField = new javax.swing.JFormattedTextField();
        separator2 = new javax.swing.JSeparator();
        landscapeCheckBox = new javax.swing.JCheckBox();
        impose4upCheckBox = new javax.swing.JCheckBox();
        scaleCheckBox = new javax.swing.JCheckBox();
        scaleTextField = new javax.swing.JTextField();
        separator1 = new javax.swing.JSeparator();
        runButton = new javax.swing.JButton();
        closeButton = new javax.swing.JButton();
        outputScrollPane = new javax.swing.JScrollPane();
        outputTextArea = new javax.swing.JTextArea();
        separator3 = new javax.swing.JSeparator();
        cleanupCheckBox = new javax.swing.JCheckBox();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("YiPDF");
        setLocationByPlatform(true);

        jPanel1.setBorder(javax.swing.BorderFactory.createEmptyBorder(4, 4, 4, 4));
        jPanel1.setLayout(new java.awt.GridBagLayout());

        fileLabel.setText("PDF file");
        jPanel1.add(fileLabel, new java.awt.GridBagConstraints());
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.weightx = 1.0;
        jPanel1.add(fileField, gridBagConstraints);

        browseButton.setText("...");
        browseButton.setToolTipText("\"Browse file system\"");
        browseButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                browseButtonActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        jPanel1.add(browseButton, gridBagConstraints);

        trimCheckBox.setSelected(true);
        trimCheckBox.setText("Trim");
        trimCheckBox.setToolTipText("Check this if you want to trim input content (i.e. remove margins you find too big)");
        trimCheckBox.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                trimCheckBoxActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 2;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        jPanel1.add(trimCheckBox, gridBagConstraints);

        trimTextField.setText("10mm 10mm 10mm 10mm");
        trimTextField.setToolTipText("Specify margins top clip: Left Bottom Right Top");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 2;
        gridBagConstraints.gridwidth = 3;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.weightx = 1.0;
        jPanel1.add(trimTextField, gridBagConstraints);
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 5;
        gridBagConstraints.gridwidth = 4;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4);
        jPanel1.add(separator2, gridBagConstraints);

        landscapeCheckBox.setSelected(true);
        landscapeCheckBox.setText("Landscape");
        landscapeCheckBox.setToolTipText("Check this if input file is in landscape mode");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 3;
        gridBagConstraints.gridy = 0;
        jPanel1.add(landscapeCheckBox, gridBagConstraints);

        impose4upCheckBox.setSelected(true);
        impose4upCheckBox.setText("4up");
        impose4upCheckBox.setToolTipText("Check this to transform the pdf so 4 pages fit into 1");
        impose4upCheckBox.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                impose4upCheckBoxActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 3;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        jPanel1.add(impose4upCheckBox, gridBagConstraints);

        scaleCheckBox.setSelected(true);
        scaleCheckBox.setText("Scale");
        scaleCheckBox.setToolTipText("Check this to scale final output (i.e. add margins to final document)");
        scaleCheckBox.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                scaleCheckBoxActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 4;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        jPanel1.add(scaleCheckBox, gridBagConstraints);

        scaleTextField.setText("0.9");
        scaleTextField.setToolTipText("Scale value, enter any value from 0.0 to 1.0");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 4;
        gridBagConstraints.gridwidth = 3;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.weightx = 1.0;
        jPanel1.add(scaleTextField, gridBagConstraints);
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.gridwidth = 4;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4);
        jPanel1.add(separator1, gridBagConstraints);

        runButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/yipdf/icon_go.png"))); // NOI18N
        runButton.setText("Run");
        runButton.setToolTipText("This will run commands to generate the desired PDF output document");
        runButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                runButtonActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 6;
        gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4);
        jPanel1.add(runButton, gridBagConstraints);

        closeButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/yipdf/icon_cancel.png"))); // NOI18N
        closeButton.setText("Close");
        closeButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                closeButtonActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 3;
        gridBagConstraints.gridy = 9;
        gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4);
        jPanel1.add(closeButton, gridBagConstraints);

        outputTextArea.setColumns(20);
        outputTextArea.setRows(5);
        outputScrollPane.setViewportView(outputTextArea);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 7;
        gridBagConstraints.gridwidth = 4;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.weighty = 1.0;
        jPanel1.add(outputScrollPane, gridBagConstraints);
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 8;
        gridBagConstraints.gridwidth = 4;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4);
        jPanel1.add(separator3, gridBagConstraints);

        cleanupCheckBox.setSelected(true);
        cleanupCheckBox.setText("Delete temporary files afterwards");
        cleanupCheckBox.setToolTipText("Check this if you don't want to see intermediate files");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 6;
        gridBagConstraints.gridwidth = 3;
        jPanel1.add(cleanupCheckBox, gridBagConstraints);

        getContentPane().add(jPanel1, java.awt.BorderLayout.CENTER);

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void scaleCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_scaleCheckBoxActionPerformed
        scaleTextField.setEnabled(scaleCheckBox.isSelected());
    }//GEN-LAST:event_scaleCheckBoxActionPerformed

    private void trimCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_trimCheckBoxActionPerformed
        trimTextField.setEnabled(trimCheckBox.isSelected());
    }//GEN-LAST:event_trimCheckBoxActionPerformed

    private void impose4upCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_impose4upCheckBoxActionPerformed
        scaleCheckBox.setEnabled(impose4upCheckBox.isSelected());
        scaleCheckBox.setSelected(impose4upCheckBox.isSelected());
        scaleTextField.setEnabled(impose4upCheckBox.isSelected());
    }//GEN-LAST:event_impose4upCheckBoxActionPerformed

    private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed
        JFileChooser chooser = new JFileChooser();
        chooser.setFileFilter(new FileNameExtensionFilter("PDF documents", "pdf"));
        int returnVal = chooser.showOpenDialog(this);
        if (returnVal == JFileChooser.APPROVE_OPTION) {
            fileField.setText(chooser.getSelectedFile().getAbsolutePath());
        }
    }//GEN-LAST:event_browseButtonActionPerformed

    private void closeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeButtonActionPerformed
        //dispose();
        System.exit(0);// just in case background thread is stuck because of weird parameters...
    }//GEN-LAST:event_closeButtonActionPerformed

    private void runButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_runButtonActionPerformed
        runButton.setEnabled(false);
        closeButton.setEnabled(false);
        if (taskResult_!=null && !taskResult_.isDone()) {
            JOptionPane.showMessageDialog(this, "Still working...", "Error", JOptionPane.ERROR_MESSAGE);
            return;
        }
        final Runnable reenableButtons = new Runnable() {
            public void run() {
                runButton.setEnabled(true);
                closeButton.setEnabled(true);
            }
        };
        Callable<String> work = new Callable<String>() {

            public String call() throws Exception {
                String result = gatherParametersAndRunCommand();
                SwingUtilities.invokeLater(reenableButtons);
                return result;
            }
        };
        execService_.submit(work);
    }//GEN-LAST:event_runButtonActionPerformed

    private String gatherParametersAndRunCommand() {
        String path = fileField.getText();
        if (path == null) {
            return "Please choose a file first";
        }
        File file = new File(path);
        if (!file.exists()) {
            return "File does not exist: " + file;
        }
        if (!file.canRead()) {
            return "Permission error, cannot read file " + file;
        }
        if (!file.getParentFile().canWrite()) {
            return "Permission error, cannot write to parent directory "+file.getParent();
        }
        float scale = -1.0F;
        try {
            scale = Float.valueOf(scaleTextField.getText());
        }
        catch (NumberFormatException e) {
            return "Error: invalid scale ("+e.getMessage()+")";
        }
        if (scale>=1.0F) {
            scale = -1.0F;
        }
        return runPdfJam(
                file,
                landscapeCheckBox.isSelected(),
                trimTextField.getText(),
                impose4upCheckBox.isSelected(),
                scale,
                cleanupCheckBox.isSelected()
                );
    }

    private String runPdfJam(File file, boolean landscape, String trimOption, boolean imposition4up, float scale, boolean cleanup) {
        String name = file.getName().substring(0, file.getName().length() - 4);
        List<String> cmd = new ArrayList<String>();
        File tmpIn=file,tmpOut;
        // step #1 : trim
        if (trimOption != null) {
            tmpOut = new File(tmpIn.getParentFile(), name + "-trim.pdf");
            cmd.clear();
            cmd.add("pdfjam");
            if (landscape) {
                cmd.add("--landscape");
            }
            cmd.add("--trim");
            cmd.add(trimOption);
            cmd.add("--clip");
            cmd.add("true");
            cmd.add(tmpIn.getAbsolutePath());
            cmd.add("-o");
            cmd.add(tmpOut.getAbsolutePath());
            String status = runCommand("Trim", cmd);
            if (!"OK".equals(status)) {
                return status;
            }
            tmpIn=tmpOut;
        }
        // step #2 : imposition (4up)
        if (imposition4up) {
            tmpOut = new File(tmpIn.getParentFile(), name + "-4up-tmp.pdf");
            cmd.clear();
            cmd.add("pdfjam");
            cmd.add("--nup");
            cmd.add("--1x2");
            cmd.add(tmpIn.getAbsolutePath());
            cmd.add("-o");
            cmd.add(tmpOut.getAbsolutePath());
            String status = runCommand("4up#1", cmd);
            if (!"OK".equals(status)) {
                return status;
            }
            if (cleanup) tmpIn.delete();
            tmpIn=tmpOut;
            tmpOut = new File(tmpIn.getParentFile(), name + "-4up.pdf");
            cmd.clear();
            cmd.add("pdfjam");
            if (landscape) {
                cmd.add("--landscape");
            }
            cmd.add("--nup");
            cmd.add("--2x1");
            if (scale>0.0F) {
                cmd.add("--scale");
                cmd.add(String.valueOf(scale));
            }
            cmd.add(tmpIn.getAbsolutePath());
            cmd.add("-o");
            cmd.add(tmpOut.getAbsolutePath());
            status = runCommand("4up#2", cmd);
            if (!"OK".equals(status)) {
                return status;
            }
            if (cleanup) tmpIn.delete();
            tmpIn=tmpOut;
        }
        return "OK";
    }

    private String runCommand(String step, List<String> cmd) {
        try {
            ProcessBuilder builder = new ProcessBuilder(cmd);
            Process p = builder.start();
            CountDownLatch latch = startPrinterThreads(p, "PDFTrim");
            p.waitFor();
        } catch (InterruptedException ex) {
            String message = "Interrupted at step '"+step+"'";
            Logger.getLogger(YiPDF.class.getName()).log(Level.SEVERE, message, ex);
            return message;
        } catch (IOException ex) {
            String message = "I/O error at step '"+step+"'";
            Logger.getLogger(YiPDF.class.getName()).log(Level.SEVERE, message, ex);
            return message;
        }
        return "OK";
    }

    private void print(final String s) {
        print(s, OUTPUT_STYLE);
    }

    private void print(final String s, final AttributeSet attr) {
        Runnable task = new Runnable() {

            public void run() {
                Document doc = outputTextArea.getDocument();
                try {
                    doc.insertString(doc.getLength(), s+"\n", attr);
                } catch (BadLocationException ex) {
                    Logger.getLogger(YiPDF.class.getName()).log(Level.SEVERE, "Failed to append string " + s, ex);
                }
            }
        };
        if (SwingUtilities.isEventDispatchThread()) {
            task.run();
        } else {
            SwingUtilities.invokeLater(task);
        }
    }

    /*
     * Start 2 threads to process output streams.
     * Return a latch that is "released" when both threads have finished their work.
     */
    private CountDownLatch startPrinterThreads(final Process process, final String cmd) {

        final CountDownLatch latch = new CountDownLatch(2); // 2 tasks need to be finished before release

        Runnable stdErrDumper = new Runnable() {

            public void run() {
                InputStream is = process.getErrorStream();
                LineNumberReader r = new LineNumberReader(new InputStreamReader(is));
                String line = "";
                try {
                    while (line != null) {
                        line = r.readLine();
                        if (line != null) {
                            print(line, OUTPUT_ERROR_STYLE);
                        }
                    }
                } catch (IOException e) {
                    Logger.getLogger(YiPDF.class.getName()).log(Level.SEVERE, "Failed to read error output stream for " + cmd, e);
                } finally {
                    try {
                        r.close();
                    } catch (IOException e) {
                        Logger.getLogger(YiPDF.class.getName()).log(Level.SEVERE, "Failed to close process stderr", e);
                    }
                }
                latch.countDown();
            }
        };

        Runnable stdOutDumper = new Runnable() {

            public void run() {
                InputStream is = process.getInputStream();
                LineNumberReader r = new LineNumberReader(new InputStreamReader(is));
                String line = "";
                try {
                    while (line != null) {
                        line = r.readLine();
                        if (line != null) {
                            print(line);
                        }
                    }
                    print("Exited: " + cmd, OUTPUT_SPECIAL_STYLE);
                } catch (IOException e) {
                    Logger.getLogger(YiPDF.class.getName()).log(Level.SEVERE, "Failed to read standard output stream for " + cmd, e);
                } finally {
                    try {
                        r.close();
                    } catch (IOException e) {
                        Logger.getLogger(YiPDF.class.getName()).log(Level.SEVERE, "Failed to close process stdout", e);
                    }
                }
                latch.countDown();
            }
        };

        Thread thread = new Thread(stdErrDumper, "stderrDumper");
        thread.start();
        thread = new Thread(stdOutDumper, "stdoutDumper");
        thread.start();
        return latch;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {

            public void run() {
                YiPDF frame = new YiPDF();
                frame.setTitle("YiPDF - "+VERSION);
                frame.pack();
                frame.setVisible(true);
            }
        });
    }
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton browseButton;
    private javax.swing.JCheckBox cleanupCheckBox;
    private javax.swing.JButton closeButton;
    private javax.swing.JTextField fileField;
    private javax.swing.JLabel fileLabel;
    private javax.swing.JCheckBox impose4upCheckBox;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JCheckBox landscapeCheckBox;
    private javax.swing.JScrollPane outputScrollPane;
    private javax.swing.JTextArea outputTextArea;
    private javax.swing.JButton runButton;
    private javax.swing.JCheckBox scaleCheckBox;
    private javax.swing.JTextField scaleTextField;
    private javax.swing.JSeparator separator1;
    private javax.swing.JSeparator separator2;
    private javax.swing.JSeparator separator3;
    private javax.swing.JCheckBox trimCheckBox;
    private javax.swing.JFormattedTextField trimTextField;
    // End of variables declaration//GEN-END:variables
    private ExecutorService execService_ = Executors.newSingleThreadExecutor();
    private Future<String> taskResult_;
    private static SimpleAttributeSet OUTPUT_STYLE = new SimpleAttributeSet();
    private static SimpleAttributeSet OUTPUT_ERROR_STYLE = new SimpleAttributeSet();
    private static SimpleAttributeSet OUTPUT_SPECIAL_STYLE = new SimpleAttributeSet();

    static {
        StyleConstants.setBold(OUTPUT_ERROR_STYLE, true);
        StyleConstants.setForeground(OUTPUT_ERROR_STYLE, Color.RED);
        StyleConstants.setFontFamily(OUTPUT_STYLE, "Monospaced");
        StyleConstants.setFontFamily(OUTPUT_ERROR_STYLE, "Monospaced");
        StyleConstants.setItalic(OUTPUT_SPECIAL_STYLE, true);
        StyleConstants.setForeground(OUTPUT_SPECIAL_STYLE, Color.GRAY);
        StyleConstants.setFontFamily(OUTPUT_SPECIAL_STYLE, "Monospaced");
    }
}
